Skip to content

How I deploy pigallery2

After my 2021 post https://github.com/bpatrik/pigallery2/discussions/292 and my 2023 follow-up https://github.com/bpatrik/pigallery2/discussions/738 on how I use the app, I decided to finally write down how I actually deploy pigallery2.

This is not meant to be the only way to run pigallery2, but rather the setup I personally trust and use in production.

In this post I’ll show how to deploy pigallery2 using:

  • Docker
  • Portainer (container management)
  • Nginx Proxy Manager (NPM) with SSL
  • Authentik for single sign-on (optional, but recommended)

Prerequisites

Before starting, make sure you have:

  1. A domain name you control Example: https://mypigallery2.com

  2. A server you can SSH into I assume a Debian-based Linux machine (Ubuntu, Debian, etc.)

  3. Docker installed 👉 https://docs.docker.com/install/

That’s it. Everything else will run inside containers.


Preparation

I like to keep all configuration files in one place so they’re easy to back up with a simple cp or rsync.

Create a config directory in your home folder:

cd ~
mkdir config

We’ll place all service configs under this directory.


Portainer

Official docs: https://docs.portainer.io/start/install-ce/server/docker/linux#docker-compose

Portainer is a web-based Docker manager. It lets you:

  • deploy Docker Compose stacks
  • update and restart containers
  • inspect logs
  • manage volumes and networks

If you don’t pay for Portainer, you need to start it manually. The easiest way is still Docker Compose.

Create the Portainer config

mkdir portainer
cd portainer

Create a docker-compose.yml:

version: "3.9"
services:
  homer:
    image: portainer/portainer-ce:lts
    container_name: portainer
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data
    ports:
      - 9443:9443
    restart: always

volumes:
  portainer_data:
    external: true

Start it, then open:

https://localhost:9443

You should see the Portainer UI.


Nginx Proxy Manager (NPM)

Official guide: https://nginxproxymanager.com/guide/

Why NPM?

In 2025, you should not expose any app to the internet without SSL. And you should not trust any self-hosted app — including mine.

I trust nginx to:

  • terminate TLS
  • handle HTTP quirks
  • protect against basic abuse and misconfiguration

Nginx Proxy Manager makes nginx usable without turning your life into a YAML nightmare.

(An alternative would be Traefik, but NPM is easier to reason about.)


Deploy NPM via Portainer

In Portainer, create a new stack and paste this compose file:

services:
  app:
    container_name: nginx-proxy-manager
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      - '80:80'
      - '81:81'
      - '443:443'
    volumes:
      - /home/<user>/configs/nginx-proxy-manager/data:/data # change me
      - /home/<user>/configs/nginx-proxy-manager/letsencrypt:/etc/letsencrypt # change me
    networks:
      - frontend

networks:
  frontend:
    name: frontend

Replace <user> with your Linux username.

⚠️ Important note about networks

The frontend Docker network is crucial. Every service you want to expose publicly must be on the same Docker network.

In my setup, this network is called frontend. Use the same name everywhere.

kép

Deploy the stack. Make sure ports 80, 81, and 443 are free on the host.


Pigallery2

Create the container

Go back to Portainer and create another stack with the following compose file:

version: '3'
services:
  pigallery2:
    image: bpatrik/pigallery2:latest
    container_name: pigallery2
    environment:
      - NODE_ENV=production
      # - NODE_ENV=debug # uncomment for debugging
    volumes:
      - "/home/<user>/configs/pigallery2/config:/app/data/config" # CHANGE ME
      - "db-data:/app/data/db"
      - "<path>/images:/app/data/images:ro" # CHANGE ME, ':ro' means read-only
      - "<path>/tmp:/app/data/tmp" # CHANGE ME
    expose:
      - "80"
    restart: always
    networks:
      - frontend

volumes:
  db-data:

networks:
  frontend:
    name: frontend
    external: true

kép

Key points:

  • The container only exposes port 80, it does not bind it to the host
  • NPM will reach it via Docker networking
  • Images are mounted read-only
  • The DB volume is disposable (as discussed in previous posts)

Deploy the stack.


Add pigallery2 to Nginx Proxy Manager

In NPM, add a new Proxy Host.

kép kép

Advanced configuration

Paste the following into the Advanced tab:

gzip on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
  text/css
  text/plain
  text/javascript
  text/markdown
  application/javascript
  application/json
  application/gpx+xml
  application/x-javascript
  application/xml
  application/xml+rss
  application/xhtml+xml
  application/x-font-ttf
  application/x-font-opentype
  application/vnd.ms-fontobject
  image/svg+xml
  image/x-icon
  application/rss+xml
  application/atom_xml;
gzip_disable "MSIE [1-6]\.(?!.*SV1)";

# Allow all HTTP methods for the API
# See https://github.com/bpatrik/pigallery2/issues/214
location /pgapi {
  proxy_pass http://pigallery2:80;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection 'upgrade';
  proxy_set_header Host $host;
  proxy_cache_bypass $http_upgrade;
}

# UI is GET-only
location / {
  limit_except GET {
    deny all;
  }
  proxy_pass http://pigallery2:80;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection 'upgrade';
  proxy_set_header Host $host;
  proxy_cache_bypass $http_upgrade;
}

kép

Save the host. NPM will automatically request SSL certificates.

Once finished, you should be able to access:

https://mypigallery2.com

Official docs: https://docs.goauthentik.io/install-config/install/docker-compose/

If you run multiple self-hosted services, you’ll eventually want single sign-on.

Authentik (or Authelia) solves this. Authentik is heavier, but it supports Google login and richer flows.

Since https://github.com/bpatrik/pigallery2/issues/1096, pigallery2 supports OpenID Connect, so Authentik integrates cleanly.


Deploy Authentik in Portainer

I strongly recommend grabbing the latest compose file from their docs, but for reference, here’s mine:

services:
  postgresql:
    env_file:
      - stack.env
    environment:
      POSTGRES_DB: ${PG_DB:-authentik}
      POSTGRES_PASSWORD: ${PG_PASS:?database password required}
      POSTGRES_USER: ${PG_USER:-authentik}
    healthcheck:
      interval: 30s
      retries: 5
      start_period: 20s
      test:
        - CMD-SHELL
        - pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}
      timeout: 5s
    image: docker.io/library/postgres:16-alpine
    restart: unless-stopped
    volumes:
      - database:/var/lib/postgresql/data

  server:
    command: server
    depends_on:
      postgresql:
        condition: service_healthy
    env_file:
      - stack.env
    environment:
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
      AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
      AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.10.2}
    ports:
      - ${COMPOSE_PORT_HTTP:-9000}:9000
      - ${COMPOSE_PORT_HTTPS:-9443}:9443
    restart: unless-stopped
    volumes:
      - ./media:/media
      - ./custom-templates:/templates

  worker:
    command: worker
    depends_on:
      postgresql:
        condition: service_healthy
    env_file:
      - stack.env
    environment:
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
      AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
      AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.10.2}
    restart: unless-stopped
    user: root
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./media:/media
      - ./certs:/certs
      - ./custom-templates:/templates

volumes:
  database:
    driver: local

Don’t forget to define environment variables:

kép


Configure Authentik

Create a new Application:

kép

  • Name: pigallery2
  • Slug: pigallery2

kép

Select provider:

kép

  • Provider type: OAuth2 / OpenID Connect

Set callback URL:

https://mypigallery2.com/pgapi/auth/oidc/callback

kép

Other settings:

  • Subject mode: User’s hashed ID
  • Bindings: none required

Add Authentik to pigallery2

Finally, configure OIDC inside pigallery2:

kép

At this point, pigallery2 will delegate authentication to Authentik.


Closing thoughts

This setup might look heavy at first, but it gives me:

  • TLS everywhere
  • sane HTTP defaults
  • isolation between services
  • optional SSO
  • zero trust in my own code 😄

If something breaks, I can tear everything down and rebuild it — which fits perfectly with pigallery2’s philosophy of DB as cache and disk as source of truth.

Hope this helps others running pigallery2 in the wild.